local _, ns = ...
local oUF = ns.oUF or oUF

assert(oUF, "oUF_MovableFrames was unable to locate oUF install.")

-- The DB is organized as the following:
-- {
--    Lily = {
--       player = "CENTER\031UIParent\0310\031-621",
-- }
--}
local _DB
local _LOCK

local _BACKDROP = {
	bgFile = "Interface\\Tooltips\\UI-Tooltip-Background";
}

local print = function(...)
	return print('|cff33ff99oUF_MovableFrames:|r', ...)
end
local round = function(n)
	return math.floor(n * 1e5 + .5) / 1e5
end

local backdropPool = {}

local getPoint = function(obj, anchor)
	if(not anchor) then
		local UIx, UIy = UIParent:GetCenter()
		local Ox, Oy = obj:GetCenter()

		-- Frame doesn't really have a positon yet.
		if(not Ox) then return end

		local UIS = UIParent:GetEffectiveScale()
		local OS = obj:GetEffectiveScale()

		local UIWidth, UIHeight = UIParent:GetRight(), UIParent:GetTop()

		local LEFT = UIWidth / 3
		local RIGHT = UIWidth * 2 / 3

		local point, x, y
		if(Ox >= RIGHT) then
			point = 'RIGHT'
			x = obj:GetRight() - UIWidth
		elseif(Ox <= LEFT) then
			point = 'LEFT'
			x = obj:GetLeft()
		else
			x = Ox - UIx
		end

		local BOTTOM = UIHeight / 3
		local TOP = UIHeight * 2 / 3

		if(Oy >= TOP) then
			point = 'TOP' .. (point or '')
			y = obj:GetTop() - UIHeight
		elseif(Oy <= BOTTOM) then
			point = 'BOTTOM' .. (point or '')
			y = obj:GetBottom()
		else
			if(not point) then point = 'CENTER' end
			y = Oy - UIy
		end

		return string.format(
			'%s\031%s\031%d\031%d',
			point, 'UIParent', round(x * UIS / OS),  round(y * UIS / OS)
		)
	else
		local point, parent, _, x, y = anchor:GetPoint()

		return string.format(
			'%s\031%s\031%d\031%d',
			point, 'UIParent', round(x), round(y)
		)
	end
end

local getObjectInformation  = function(obj)
	-- This won't be set if we're dealing with oUF <1.3.22. Due to this we're just
	-- setting it to Unknown. It will only break if the user has multiple layouts
	-- spawning the same unit or change between layouts.
	local style = obj.style or 'Unknown'
	local identifier = obj:GetName() or obj.unit

	-- Are we dealing with header units?
	local isHeader
	local parent = obj:GetParent()

	if(parent) then
		if(parent:GetAttribute'initialConfigFunction' and parent.style) then
			isHeader = parent

			identifier = parent:GetName()
		elseif(parent:GetAttribute'oUF-onlyProcessChildren') then
			isHeader = parent:GetParent()

			identifier = isHeader:GetName()
		end
	end

	return style, identifier, isHeader
end

local restoreDefaultPosition = function(style, identifier)
	-- We've not saved any default position for this style.
	if(not _DB.__INITIAL or not _DB.__INITIAL[style] or not _DB.__INITIAL[style][identifier]) then return end

	local obj, isHeader
	for _, frame in next, oUF.objects do
		local fStyle, fIdentifier, fIsHeader = getObjectInformation(frame)
		if(fStyle == style and fIdentifier == identifier) then
			obj = frame
			isHeader = fIsHeader

			break
		end
	end

	if(obj) then
		local scale = obj:GetScale()
		local target = isHeader or obj
		local SetPoint = getmetatable(target).__index.SetPoint;

		target:ClearAllPoints()

		local point, parentName, x, y = string.split('\031', _DB.__INITIAL[style][identifier])
		SetPoint(target, point, parentName, point, x / scale, y / scale)

		local backdrop = backdropPool[target]
		if(backdrop) then
			backdrop:ClearAllPoints()
			backdrop:SetAllPoints(target)
		end

		-- We don't need this anymore
		_DB.__INITIAL[style][identifier] = nil
		if(not next(_DB.__INITIAL[style])) then
			_DB[style] = nil
		end
	end
end

local function restorePosition(obj)
	if(InCombatLockdown()) then return end
	local style, identifier, isHeader = getObjectInformation(obj)
	-- We've not saved any custom position for this style.
	if(not _DB[style] or not _DB[style][identifier]) then return end

	local scale = obj:GetScale()
	local target = isHeader or obj
	local SetPoint = getmetatable(target).__index.SetPoint;

	-- Hah, a spot you have to use semi-colon!
	-- Guess I've never experienced that as these are usually wrapped in do end
	-- statements.
	target.SetPoint = restorePosition;
	target:ClearAllPoints()

	-- damn it Blizzard, _how_ did you manage to get the input of this function
	-- reversed. Any sane person would implement this as: split(str, dlm, lim);
	local point, parentName, x, y = string.split('\031', _DB[style][identifier])
	SetPoint(target, point, parentName, point, x / scale, y / scale)
end

local saveDefaultPosition = function(obj)
	local style, identifier, isHeader = getObjectInformation(obj)
	if(not _DB.__INITIAL) then
		_DB.__INITIAL = {}
	end

	if(not _DB.__INITIAL[style]) then
		_DB.__INITIAL[style] = {}
	end

	if(not _DB.__INITIAL[style][identifier]) then
		local point
		if(isHeader) then
			point = getPoint(isHeader)
		else
			point = getPoint(obj)
		end

		_DB.__INITIAL[style][identifier] = point
	end
end

local savePosition = function(obj, anchor)
	local style, identifier, isHeader = getObjectInformation(obj)
	if(not _DB[style]) then _DB[style] = {} end

	_DB[style][identifier] = getPoint(isHeader or obj, anchor)
end

-- Attempt to figure out a more sane name to dispaly.
local smartName
do
	local nameCache = {}
	local validNames = {
		'player',
		'target',
		'focus',
		'raid',
		'pet',
		'party',
		'maintank',
		'mainassist',
		'arena',
	}

	local validName = function(smartName)
		-- Not really a valid name, but we'll accept it for simplicities sake.
		if(tonumber(smartName)) then
			return smartName
		end

		if(type(smartName) == 'string') then
			if(smartName == 'mt') then
				return 'maintank'
			end

			for _, v in next, validNames do
				if(v == smartName) then
					return smartName
				end
			end

			if(
				smartName:match'^party%d?$' or
				smartName:match'^arena%d?$' or
				smartName:match'^boss%d?$' or
				smartName:match'^partypet%d?$' or
				smartName:match'^raid%d?%d?$' or
				smartName:match'%w+target$' or
				smartName:match'%w+pet$'
				) then
				return smartName
			end
		end
	end

	local function guessName(...)
		local name = validName(select(1, ...))

		local n = select('#', ...)
		if(n > 1) then
			for i=2, n do
				local inp = validName(select(i, ...))
				if(inp) then
					name = (name or '') .. inp
				end
			end
		end

		return name
	end

	local smartString = function(name)
		if(nameCache[name]) then
			return nameCache[name]
		end

		-- Here comes the substitute train!
		local n = name:gsub('(%l)(%u)', '%1_%2'):gsub('([%l%u])(%d)', '%1_%2_'):lower()
		n = guessName(string.split('_', n))
		if(n) then
			nameCache[name] = n
			return n
		end

		return name
	end

	smartName = function(obj, header)
		if(type(obj) == 'string') then
			return smartString(obj)
		elseif(header) then
			return smartString(header:GetName())
		else
			local name = obj:GetName()
			if(name) then
				return smartString(name)
			end

			return obj.unit or '<unknown>'
		end
	end
end

do
	local frame = CreateFrame"Frame"
	frame:SetScript("OnEvent", function(self, event)
		return self[event](self)
	end)

	function frame:VARIABLES_LOADED()
		-- I honestly don't trust the load order of SVs.
		_DB = DarkDuranceDB or {}
		DarkDuranceDB = _DB
		-- Got to catch them all!
		for _, obj in next, oUF.objects do
			restorePosition(obj)
		end

		oUF:RegisterInitCallback(restorePosition)
		self:UnregisterEvent"VARIABLES_LOADED"
	end
	frame:RegisterEvent"VARIABLES_LOADED"

	function frame:PLAYER_REGEN_DISABLED()
		if(_LOCK) then
			print("Anchors hidden due to combat.")
			for k, bdrop in next, backdropPool do
				bdrop:Hide()
			end
			_LOCK = nil
		end
	end
	frame:RegisterEvent"PLAYER_REGEN_DISABLED"
end

local getBackdrop
do
	local OnShow = function(self)
		return self.name:SetText(smartName(self.obj, self.header))
	end

	local OnDragStart = function(self)
		saveDefaultPosition(self.obj)
		self:StartMoving()

		local frame = self.header or self.obj
		frame:ClearAllPoints();
		frame:SetAllPoints(self);
	end

	local OnDragStop = function(self)
		self:StopMovingOrSizing()
		savePosition(self.obj, self)
	end

	getBackdrop = function(obj, isHeader)
		local target = isHeader or obj
		if(not target:GetCenter()) then return end
		if(backdropPool[target]) then return backdropPool[target] end

		local backdrop = CreateFrame"Frame"
		backdrop:SetParent(UIParent)
		backdrop:Hide()

		backdrop:SetBackdrop(_BACKDROP)
		backdrop:SetFrameStrata"TOOLTIP"
		backdrop:SetAllPoints(target)

		backdrop:EnableMouse(true)
		backdrop:SetMovable(true)
		backdrop:RegisterForDrag"LeftButton"

		backdrop:SetScript("OnShow", OnShow)

		local name = backdrop:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		name:SetPoint"CENTER"
		name:SetJustifyH"CENTER"
		name:SetFont(GameFontNormal:GetFont(), 12)
		name:SetTextColor(1, 1, 1)

		backdrop.name = name
		backdrop.obj = obj
		backdrop.header = isHeader

		backdrop:SetBackdropBorderColor(0, .9, 0)
		backdrop:SetBackdropColor(0, .9, 0)

		-- Work around the fact that headers with no units displayed are 0 in height.
		if(isHeader and math.floor(isHeader:GetHeight()) == 0) then
			local height = isHeader:GetChildren():GetHeight()
			isHeader:SetHeight(height)
		end

		backdrop:SetScript("OnDragStart", OnDragStart)
		backdrop:SetScript("OnDragStop", OnDragStop)

		backdropPool[target] = backdrop

		return backdrop
	end
end

do
	local opt = CreateFrame("Frame", nil, InterfaceOptionsFramePanelContainer)
	opt:Hide()

	opt.name = "DarkDurance UnitFrame"
	opt:SetScript("OnShow", function(self)
		local title = self:CreateFontString(nil, 'ARTWORK', 'GameFontNormalLarge')
		title:SetPoint('TOPLEFT', 16, -16)
		title:SetText(opt.name)

        local toggleButton = CreateFrame('Button', nil, opt, 'OptionsButtonTemplate')
        toggleButton:SetPoint('LEFT', title, 'RIGHT', 5, 0)
        toggleButton:SetHeight(25)
        toggleButton:SetWidth(100)
        toggleButton:SetText('Toggle Lock')
        toggleButton:SetScript('OnClick', function() SlashCmdList['DDUF_MOVE']() end)

        local resetButton = CreateFrame('Button', nil, opt, 'OptionsButtonTemplate')
        resetButton:SetPoint('LEFT', toggleButton, 'RIGHT', 5, 0)
        resetButton:SetHeight(25)
        resetButton:SetWidth(100)
        resetButton:SetText('Reset')
        resetButton:SetScript('OnClick', function() _DB=nil; DarkDuranceDB=nil; ReloadUI() end)

		local subtitle = self:CreateFontString(nil, 'ARTWORK', 'GameFontHighlightSmall')
		subtitle:SetHeight(40)
		subtitle:SetPoint('TOPLEFT', title, 'BOTTOMLEFT', 0, -8)
		subtitle:SetPoint('RIGHT', self, -32, 0)
		subtitle:SetNonSpaceWrap(true)
		subtitle:SetWordWrap(true)
		subtitle:SetJustifyH'LEFT'
		subtitle:SetText('Note that the initial frame position set by layouts are currently'
		.. ' not saved. This means that a reload of the UI is required to correctly reset'
		.. ' the position after deleting an element.')

		local scroll = CreateFrame("ScrollFrame", nil, self)
		scroll:SetPoint('TOPLEFT', subtitle, 'BOTTOMLEFT', 0, -8)
		scroll:SetPoint("BOTTOMRIGHT", 0, 4)

		local scrollchild = CreateFrame("Frame", nil, self)
		scrollchild:SetPoint"LEFT"
		scrollchild:SetHeight(scroll:GetHeight())
		scrollchild:SetWidth(scroll:GetWidth())

		scroll:SetScrollChild(scrollchild)
		scroll:UpdateScrollChildRect()
		scroll:EnableMouseWheel(true)

		local slider = CreateFrame("Slider", nil, scroll)

		local backdrop = {
			bgFile = [[Interface\ChatFrame\ChatFrameBackground]], tile = true, tileSize = 16,
			edgeFile = [[Interface\Tooltips\UI-Tooltip-Border]], edgeSize = 16,
			insets = {left = 4, right = 4, top = 4, bottom = 4},
		}

		local createOrUpdateMadnessOfGodIhateGUIs
		local OnClick = function(self)
			scroll.value = slider:GetValue()
			_DB[self.style][self.ident] = nil

			if(not next(_DB[self.style])) then
				_DB[self.style] = nil
			end

			restoreDefaultPosition(self.style, self.ident)

			return createOrUpdateMadnessOfGodIhateGUIs()
		end

		local OnEnter = function(self)
			GameTooltip:SetOwner(self, "ANCHOR_RIGHT");
			GameTooltip:SetText(DELETE)
		end

		function createOrUpdateMadnessOfGodIhateGUIs()
			local data = self.data or {}

			local slideHeight = 0
			local numStyles = 1
			for style, styleData in next, _DB do
				if(style ~= '__INITIAL') then
					if(not data[numStyles]) then
						local box = CreateFrame('Frame', nil, scrollchild)
						box:SetBackdrop(backdrop)
						box:SetBackdropColor(.1, .1, .1, .5)
						box:SetBackdropBorderColor(.3, .3, .3, 1)

						if(numStyles == 1) then
							box:SetPoint('TOP', 0, -12)
						else
							box:SetPoint('TOP', data[numStyles - 1], 'BOTTOM', 0, -16)
						end
						box:SetPoint'LEFT'
						box:SetPoint('RIGHT', -30, 0)

						local title = box:CreateFontString(nil, 'ARTWORK', 'GameFontHighlightSmall')
						title:SetPoint('BOTTOMLEFT', box, 'TOPLEFT', 8, 0)
						box.title = title

						data[numStyles] = box
					end

					-- Fetch the box and update it
					local box = data[numStyles]
					box.title:SetText(style)

					local rows = box.rows or {}
					local numFrames = 1
					for unit, points in next, styleData do
						if(not rows[numFrames]) then
							local row = CreateFrame('Button', nil, box)

							row:SetBackdrop(backdrop)
							row:SetBackdropBorderColor(.3, .3, .3)
							row:SetBackdropColor(.1, .1, .1, .5)

							if(numFrames == 1) then
								row:SetPoint('TOP', 0, -8)
							else
								row:SetPoint('TOP', rows[numFrames-1], 'BOTTOM')
							end

							row:SetPoint('LEFT', 6, 0)
							row:SetPoint('RIGHT', -25, 0)
							row:SetHeight(24)

							local anchor = row:CreateFontString(nil, nil, 'GameFontHighlight')
							anchor:SetPoint('RIGHT', -10, 0)
							anchor:SetPoint('TOP', 0, -4)
							anchor:SetPoint'BOTTOM'
							anchor:SetJustifyH'RIGHT'
							row.anchor = anchor

							local label = row:CreateFontString(nil, nil, 'GameFontHighlight')
							label:SetPoint('LEFT', 10, 0)
							label:SetPoint('RIGHT', anchor)
							label:SetPoint('TOP', 0, -4)
							label:SetPoint'BOTTOM'
							label:SetJustifyH'LEFT'
							row.label = label

							local delete = CreateFrame("Button", nil, row)
							delete:SetWidth(16)
							delete:SetHeight(16)
							delete:SetPoint('LEFT', row, 'RIGHT')

							delete:SetNormalTexture[[Interface\Buttons\UI-Panel-MinimizeButton-Up]]
							delete:SetPushedTexture[[Interface\Buttons\UI-Panel-MinimizeButton-Down]]
							delete:SetHighlightTexture[[Interface\Buttons\UI-Panel-MinimizeButton-Highlight]]

							delete:SetScript("OnClick", OnClick)
							delete:SetScript("OnEnter", OnEnter)
							delete:SetScript("OnLeave", GameTooltip_Hide)
							row.delete = delete

							rows[numFrames] = row
						end

						-- Fetch row and update it:
						local row = rows[numFrames]
						local point, _, x, y = string.split('\031', points)
						row.anchor:SetFormattedText('%11s %4s %4s', point, x, y)
						row.label:SetText(smartName(unit))

						row.delete.style = style
						row.delete.ident = unit
						row:Show()

						numFrames = numFrames + 1
					end

					box.rows = rows

					local height = (numFrames * 24) - 8
					slideHeight = slideHeight + height + 16
					box:SetHeight(height)
					box:Show()

					-- Hide left over rows we aren't using:
					while(rows[numFrames]) do
						rows[numFrames]:Hide()
						numFrames = numFrames + 1
					end

					numStyles = numStyles + 1
				end
			end

			-- Hide left over boxes we aren't using:
			while(data[numStyles]) do
				data[numStyles]:Hide()
				numStyles = numStyles + 1
			end

			self.data = data
			local height = slideHeight - scroll:GetHeight()
			if(height > 0) then
				slider:SetMinMaxValues(0, height)
			else
				slider:SetMinMaxValues(0, 0)
			end

			slider:SetValue(scroll.value or 0)
		end

		slider:SetWidth(16)

		slider:SetPoint("TOPRIGHT", -8, -24)
		slider:SetPoint("BOTTOMRIGHT", -8, 24)

		local up = CreateFrame("Button", nil, slider)
		up:SetPoint("BOTTOM", slider, "TOP")
		up:SetWidth(16)
		up:SetHeight(16)
		up:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up")
		up:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down")
		up:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Disabled")
		up:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight")

		up:GetNormalTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		up:GetPushedTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		up:GetDisabledTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		up:GetHighlightTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		up:GetHighlightTexture():SetBlendMode("ADD")

		up:SetScript("OnClick", function(self)
			local box = self:GetParent()
			box:SetValue(box:GetValue() - box:GetHeight()/2)
		end)

		local down = CreateFrame("Button", nil, slider)
		down:SetPoint("TOP", slider, "BOTTOM")
		down:SetWidth(16)
		down:SetHeight(16)
		down:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")
		down:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down")
		down:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Disabled")
		down:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Highlight")

		down:GetNormalTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		down:GetPushedTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		down:GetDisabledTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		down:GetHighlightTexture():SetTexCoord(1/4, 3/4, 1/4, 3/4)
		down:GetHighlightTexture():SetBlendMode("ADD")

		down:SetScript("OnClick", function(self)
			local box = self:GetParent()
			box:SetValue(box:GetValue() + box:GetHeight()/2)
		end)

		slider:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
		local thumb = slider:GetThumbTexture()
		thumb:SetWidth(16)
		thumb:SetHeight(24)
		thumb:SetTexCoord(1/4, 3/4, 1/8, 7/8)

		slider:SetScript("OnValueChanged", function(self, val, ...)
			local min, max = self:GetMinMaxValues()
			if(val == min) then up:Disable() else up:Enable() end
			if(val == max) then down:Disable() else down:Enable() end

			scroll.value = val
			scroll:SetVerticalScroll(val)
			scrollchild:SetPoint('TOP', 0, val)
		end)

		opt:SetScript("OnShow", function()
			return createOrUpdateMadnessOfGodIhateGUIs()
		end)

		return createOrUpdateMadnessOfGodIhateGUIs()
	end)

	InterfaceOptions_AddCategory(opt)
end

SLASH_DDUF_MOVE1 = '/dd'
SlashCmdList['DDUF_MOVE'] = function(inp)
	if(InCombatLockdown()) then
		return print"Frames cannot be moved while in combat. Bailing out."
	end

	if(inp:match("%S+")) then
		InterfaceOptionsFrame_OpenToCategory'oUF: MovableFrames'
	else
		if(not _LOCK) then
			for k, obj in next, oUF.objects do
				local style, identifier, isHeader = getObjectInformation(obj)
				local backdrop = getBackdrop(obj, isHeader)
				if(backdrop) then backdrop:Show() end
			end

			_LOCK = true
		else
			for k, bdrop in next, backdropPool do
				bdrop:Hide()
			end

			_LOCK = nil
		end
	end
end
-- It's not in your best interest to disconnect me. Someone could get hurt.
